"""
DescCompiler module, contains Compiler class that allows
to compile descs to intermediate structure for further code generation.
Desc program example:
    [desc_attribute("params", 1, 2)]
    desc DescName {
        string name = "default value";

        [var_attribue]
        int value;
    }

    struct StructName {
        int [] array;
        set<int> aSet;
    }

DescContext:
    list<Type>    descs
    list<Type>    structs
    list<Enum>    enums

Attrib:
    string        name
    list<string>  args        = None

Type:
    list<Attrib>  attribs     = None
    string        name
    string        parent      = None
    list<Var>     vars

Var:
    list<Attrib>  attribs     = None
    TypeInfo      type
    string        name
    string        defValue

TypeInfo:
    int           type
    # TYPE_NAME        = 1
    string        name
    # TYPE_SUBNAME     = 2
    TypeInfo      subtype
    string        name
    # TYPE_TEMPLATE    = 3
    TypeInfo      subtype
    list<TypeInfo> args
    # TYPE_ARRAY       = 4
    TypeInfo      subtype

"""

from S3D import *
from S3D.Parser import Parser, ParserData
from unittest import TestCase
from S3D.ExpressionCompiler import ExprCompiler, Expr

class TypeInfo:
    NAME        = 1
    SUBNAME     = 2
    TEMPLATE    = 3
    ARRAY       = 4

    VARS        = dict(
        NAME        = ['name'],
        SUBNAME     = ['subtype', 'name'],
        TEMPLATE    = ['subtype', 'args'],
        ARRAY       = ['subtype']
    )

    def __init__(self, type, *args, **kw):
        assert len(args) == 0 or len(kw) == 0

        self.type = getattr(TypeInfo, type)
        if len(args) != 0:
            for name, val in zip(TypeInfo.VARS[type], args):
                setattr(self, name, val)
        else:
            for name in TypeInfo.VARS[type]:
                setattr(self, name, kw[name])

    def __repr__(self):
        if self.type == TypeInfo.ARRAY:
            return "%s[]" % self.subtype
        if self.type == TypeInfo.SUBNAME:
            return "%s.%s" % (self.subtype, self.name)
        if self.type == TypeInfo.TEMPLATE:
            return "%s%s" % (self.subtype, self.args)
        return self.name

    def buildTypeName(self):
        if self.type == TypeInfo.NAME:
            return BuildName(self.name)
        if self.type == TypeInfo.SUBNAME:
            return BuildSubname(self.subtype.buildTypeName(), self.name)
        if self.type == TypeInfo.ARRAY:
            return BuildArrayName(self.subtype.buildTypeName())
        if self.type == TypeInfo.TEMPLATE:
            return BuildTemplateArgs(self.subtype.buildTypeName(), [p.buildTypeName() for p in self.args])
        return str(self)
    
class DescCompiler(Parser):
    def parse(self):
        self.desc_list = []
        self.struct_list = []
        self.enum_list = []

        while not self.nextIsToken('eof'):
            self.parseDecl()

        ctxt = ParserData(
            descs      = self.desc_list,
            structs    = self.struct_list,
            enums      = self.enum_list)

        return ctxt

    def parseDecl(self):
        attribs = self.parseAttribs()

        if self.nextIsWord('desc'):
            self.parseType(attribs)
        elif self.nextIsWord('struct'):
            self.parseType(attribs)
        elif self.nextIsWord('enum'):
            self.parseEnum(attribs)
        else:
            self.raiseError('declaration of desc/struct/enum expected')
    
    def parseType(self, attribs):
        res = ParserData(attribs = attribs)

        type = self.matchWord('struct', 'desc')
        res.name = self.matchWord()

        res.parent = None
        if self.tryMatchSymbol(':'):
            res.parent = self.matchWord()

        self.matchSymbol('{')
        res.vars = []
        while not self.tryMatchSymbol('}'):
            res.vars.append(self.parseVarDecl())

        if type == 'desc':
            self.desc_list.append(res)
        else:
            self.struct_list.append(res)

    def parseEnum(self, attribs):
        res = ParserData(attribs = attribs)

        self.matchWord('enum')
        res.name = self.matchWord()

        self.matchSymbol('{')
        res.values = []

        while not self.tryMatchSymbol('}'):
            attribs = self.parseAttribs()
            val = ParserData(name = self.matchWord(), attribs = attribs)
            res.values.append(val)

            if not self.tryMatchSymbol(','):
                self.matchSymbol('}')
                break

        self.enum_list.append(res)

    def parseVarDecl(self):
        var = ParserData(
            attribs = self.parseAttribs(),
            type    = self.parseTypeInfo(),
            name    = self.matchWord(),
            defVal  = None)
        
        if self.tryMatchSymbol('='):
            exprComp = ExprCompiler()
            var.defVal = exprComp.compileNested(self) # self.matchValue()

        self.matchSymbol(';')
        return var

    def parseTypeInfo(self):
        type = TypeInfo('NAME', self.matchWord())
        type = self.parseTypeInfoTemplate(type)

        while True:
            if self.tryMatchSymbol('.'):
                name = self.matchWord()
                type = TypeInfo('SUBNAME', name = name, subtype = type)
                type = self.parseTypeInfoTemplate(type)
            elif self.tryMatchOperator('[]'):
                type = TypeInfo('ARRAY', type)
            else:
                break

        return type

    def parseTypeInfoTemplate(self, type):
        if not self.tryMatchSymbol('<'):
            return type

        args = [self.parseTypeInfo()]
        while not self.tryMatchSymbol('>'):
            self.matchSymbol(',')
            args.append(self.parseTypeInfo())

        return TypeInfo('TEMPLATE', type, args)

    def parseAttribs(self):
        if not self.nextIsSymbol('['):
            return None

        res = []

        while self.tryMatchSymbol('['):
            attrib = ParserData(
                name = self.matchWord(),
                args = None)

            if self.tryMatchSymbol('('):
                attrib.args = [self.matchValue()]

                while not self.tryMatchSymbol(')'):
                    self.matchSymbol(',')
                    attrib.args.append(self.matchValue())

            self.matchSymbol(']')
            res.append(attrib)

        return res

class Tests(TestCase):
    def setUp(self):
        self.c = DescCompiler()

    def tearDown(self):
        self.c = None

    def check(self, obj, **kw):
        for k, v in kw.items():
            self.assert_(hasattr(obj, k))
            self.assertEqual(getattr(obj, k), v)

    def testStruct(self):
        r = self.c.compile("""
            struct MyParent {}
            struct MyStruct : MyParent {}
        """)

        self.assertIsNotNone(r, self.c.errorMsg)
        self.assertEqual(len(r.structs), 2)
        self.check(r.structs[0], name = 'MyParent')
        self.check(r.structs[1], name = 'MyStruct', parent = 'MyParent')

    def testDesc(self):
        r = self.c.compile("""
            desc MyParent {}
            desc MyStruct : MyParent {}
        """)

        self.assertIsNotNone(r, self.c.errorMsg)
        self.assertEqual(len(r.descs), 2)
        self.check(r.descs[0], name = 'MyParent')
        self.check(r.descs[1], name = 'MyStruct', parent = 'MyParent')

    def testEnum(self):
        r = self.c.compile("""
            enum MyEnum {
                E_1, E_2,
            }
            enum MyEnum2 {
                T_1, T_2, T_3
            }
        """)

        self.assertIsNotNone(r, self.c.errorMsg)
        self.assertEqual(len(r.enums), 2)
        self.check(r.enums[0], name = 'MyEnum')
        self.check(r.enums[1], name = 'MyEnum2')
        self.assertEqual([p.name for p in r.enums[0].values], ['E_1', 'E_2'])
        self.assertEqual([p.name for p in r.enums[1].values], ['T_1', 'T_2', 'T_3'])

    def testVars(self):
        r = self.c.compile("""
            desc MyDesc {
                string [][] array;
                int intval = 10;
                set<string> heel; 
                set<string [], int> heel2;
                ds.map<string>[] test;
                int otherval = a + 10 * rand(-100.5);
            }
        """)

        self.assertIsNotNone(r, self.c.errorMsg)
        self.assertEqual(len(r.descs), 1)

        v = r.descs[0]
        self.check(v, name = 'MyDesc')

        vars = [(str(p.type), p.name, p.defVal) for p in v.vars]
        self.assertEqual(vars[0], ('string[][]',          'array',    None))
        self.assertEqual(vars[1], ('int',                 'intval',   Expr.makeValue(10)))
        self.assertEqual(vars[2], ('set[string]',         'heel',     None))
        self.assertEqual(vars[3], ('set[string[], int]',  'heel2',    None))
        self.assertEqual(vars[4], ('ds.map[string][]',    'test',     None))
        self.assertEqual(vars[5], ('int',                 'otherval',
            Expr.makeBinOp(
                '+',
                Expr.makeVar('a'),
                Expr.makeBinOp(
                    '*',
                    Expr.makeValue(10),
                    Expr.makeCall(Expr.makeVar('rand'), [
                        Expr.makeUnarOp('-', Expr.makeValue(100.5))
                    ])
                )
            )
        ))

    def testAttribs(self):
        r = self.c.compile("""
            [attr1][attr2]
            enum MyEnum {
                [attr4][attr5]
                E_1,
                E_2,
            }
            
            [attr3]
            struct MyStruct {}

            [attr6(val, "hello world", +1, -20.5)]
            desc MyDesc {}
        """)

        self.assertIsNotNone(r, self.c.errorMsg)
        self.assertEqual(len(r.enums),      1)
        self.assertEqual(len(r.structs),    1)
        self.assertEqual(len(r.descs),      1)

        v = r.enums[0]
        self.check(v, name = 'MyEnum')
        self.assertEqual([p.name for p in v.attribs], ['attr1', 'attr2'])
        self.assertEqual([p.name for p in v.values], ['E_1', 'E_2'])
        self.assertIsNone(v.values[1].attribs)
        self.assertEqual(len(v.values[0].attribs), 2)
        self.assertEqual([p.name for p in v.values[0].attribs], ['attr4', 'attr5'])

        v = r.structs[0]
        self.check(v, name = 'MyStruct')
        self.assertEqual(len(v.attribs), 1)
        self.assertEqual(v.attribs[0].name, 'attr3')

        v = r.descs[0]
        self.check(v, name = 'MyDesc')
        self.assertEqual(len(v.attribs), 1)
        a = v.attribs[0]
        self.assertEqual(a.name, 'attr6')
        self.assertEqual(a.args, ['val', '"hello world"', 1, -20.5])

